use async_trait::async_trait;
use std::sync::Arc;

use crate::shared;

use super::{
    jwt::{self, MyClaims},
    request_map_manager::{self, RequestMapQuery},
};

#[async_trait]
pub trait UserActiveTokenManager: Send + Sync {
    async fn is_active_access_token(&self, username: &str, access_token: &str) -> bool;
}

pub async fn check_access_permission(
    request_method: &str,
    request_uri: &str,
    authorization_header: Option<&str>,
    user_active_token_manager: Arc<dyn UserActiveTokenManager>,
    request_map_query: Arc<dyn RequestMapQuery>,
) -> bool {
    let mut granted = false;

    // Uri returns a matched route full path.
    let url_full_path = request_uri;

    if !url_full_path.is_empty() {
        // Gets authorization from request header.
        // The string should be like below example:
        // Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2MDU3ODIxNzUsImlhdCI6MTYwNTc3ODU3NSwiaWQiOiJ1c2VyLWlkIiwidXNlcm5hbWUiOiJIdWkiLCJyb2xlcyI6WyJBZG1pbiJdfQ.tKdhYNyeqOS_ITIV_F0sKT6N-NBFjxMkPyXAkzT43WQ

        // Validates token string to see if it is a valid access token and it does not expire yet.
        let (claims, mut valid, token) = check_authorization_header(authorization_header);

        // If token is valid we still need to check if it is still active.
        if valid {
            valid = user_active_token_manager
                .is_active_access_token(&claims.as_ref().unwrap().username, &token)
                .await;
        }

        // If user was authenticated but token is no longer valid or user is not granted for current url 401 code will be sent.
        if claims.is_some() && !valid {
            granted = false;
        } else {
            // If token is valid we need to check on url access permission.
            granted = check_url_on_request_map(
                request_method,
                &url_full_path,
                &claims.as_ref(),
                request_map_query,
            )
            .await;
        }
    }

    granted
}

/// Checks on authorization header and tris to extract the claims.
fn check_authorization_header(
    authorization_header: Option<&str>,
) -> (Option<MyClaims>, bool, String) {
    let no_claims = (None, false, String::from(""));

    let header = match authorization_header {
        Some(header) => header,
        None => return no_claims,
    };

    // Fields splits the string authorizationHeader around each instance of one or more consecutive white space characters.
    let fields: Vec<_> = header.split_whitespace().collect();

    // Checks the string if it contains bearer and token.
    if fields.len() == 2 {
        // Gets token string.
        let token = fields[1];

        let (claims, valid) = jwt::validate_token(
            token,
            shared::app_config_service::APP_CONFIG
                .jwt
                .signing_key
                .as_str(),
        );

        match claims {
            Some(claims) => return (Some(claims), valid, token.to_owned()),
            None => return (None, valid, token.to_owned()),
        }
    }

    no_claims
}

/// Checks requested url on security config request map.
async fn check_url_on_request_map(
    request_method: &str,
    url: &str,
    claims: &Option<&MyClaims>,
    request_map_query: Arc<dyn RequestMapQuery>,
) -> bool {
    let mut granted = false;
    let mut configured_url: String;

    // Gets configured web root route name.
    let route_root_group_name = &shared::app_config_service::APP_CONFIG
        .server
        .route_root_group_name;

    // Removes root url.
    // For example, url is /web-api/user/get.
    // Url without root is /user/get.
    let url_without_root = url.replace(route_root_group_name, "");

    let mut temp_url: String;
    let cached_request_maps = request_map_query.query_all().await;

    for item in cached_request_maps.iter() {
        // If http request method is configured and it does not match then just ignore.
        if item.http_method.is_some() {
            let http_method = item.http_method.to_owned().unwrap();

            if !http_method.is_empty() && !request_method.eq_ignore_ascii_case(&http_method) {
                continue;
            }
        }

        // First of all we need to remove trailing wildcard.
        // For example, item.Url is /user/** or /user/*.
        // configuredUrl is /user.
        configured_url = item.url.replace("/**", "");
        configured_url = configured_url.replace("/*", "");

        // Removes the configuredUrl from urlWithoutRoot.
        // Before removing, /user/get.
        // After removing, /get.
        temp_url = url_without_root.replace(&configured_url, "");

        if temp_url.is_empty() {
            // If tempUrl is empty that means the url is exactly matched with configuredUrl.
            // Then we need to check on item.ConfigAttribute.
            granted = check_url_on_config_attribute(claims, &item.config_attribute);

            // If we find exactly match we just need to stop.
            break;
        }

        if temp_url == url_without_root {
            // If configuredUrl is /user/status then tempUrl will be the same as urlWithoutRoot.
            // That means a mismatch. We need to check on next.
            continue;
        }

        if !temp_url.starts_with("/") {
            // If tempUrl does NOT start with "/" that means the parent url does not fully match.
            // For example
            // configuredUrl: /measuringData
            // urlWithoutRoot: /measuringDataSharing/queryValidDetails
            // tempUrl will be Sharing/queryValidDetails
            continue;
        }

        // Checks if url starts with tempUrl.
        // For example /authentication is configured but NOT /api/authentication.
        // We only check wildcard config if url ends with tempUrl.
        if !&url_without_root.starts_with(&temp_url) {
            if item
                .url
                .ends_with(request_map_manager::REQUEST_MAP_URL_DOUBLE_WILDCARD.as_str())
            {
                // If tempUrl is /get or /status/check it would also match the double wildcard.
                // Then we need to check on item.ConfigAttribute.
                granted = check_url_on_config_attribute(claims, &item.config_attribute);
            } else if item
                .url
                .ends_with(request_map_manager::REQUEST_MAP_URL_SINGLE_WILDCARD.as_str())
            {
                // If tempUrl is /get it can match the single wildcard. But /status/check does NOT.
                // Then we need to check on item.ConfigAttribute.
                if temp_url.matches("/").count() == 1 {
                    granted = check_url_on_config_attribute(claims, &item.config_attribute);
                }
            }
        }
    }

    granted
}

/// Checks requested url access permission on security configAttribute.
/// This would check on authentication and authorization.
fn check_url_on_config_attribute(claims: &Option<&MyClaims>, config_attribute: &str) -> bool {
    let mut granted = false;

    if config_attribute.starts_with(request_map_manager::REQUEST_MAP_DENY_ALL.as_str()) {
        granted = false;
    } else if config_attribute.starts_with(request_map_manager::REQUEST_MAP_PERMIT_ALL.as_str()) {
        granted = true;
    } else if config_attribute
        .starts_with(request_map_manager::REQUEST_MAP_IS_AUTHENTICATED_PREFIX.as_str())
        && claims.is_some()
    {
        granted = true;
    } else if (config_attribute
        .starts_with(request_map_manager::REQUEST_MAP_HAS_ROLE_PREFIX.as_str())
        || config_attribute
            .starts_with(request_map_manager::REQUEST_MAP_HAS_ANY_ROLE_PREFIX.as_str()))
        && claims.is_some()
    {
        granted = request_map_manager::contains_any_role_in_config_attribute(
            claims.unwrap().roles.iter().map(AsRef::as_ref).collect(),
            config_attribute,
        );
    } else if (config_attribute
        .starts_with(request_map_manager::REQUEST_MAP_HAS_NO_ROLE_PREFIX.as_str())
        || config_attribute
            .starts_with(request_map_manager::REQUEST_MAP_HAS_NO_ANY_ROLE_PREFIX.as_str()))
        && claims.is_some()
    {
        granted = !request_map_manager::contains_any_role_in_config_attribute(
            claims.unwrap().roles.iter().map(AsRef::as_ref).collect(),
            config_attribute,
        );
    }

    granted
}
